#include "path.h"
#include <stdexcept>


using evm::fs::path;

constexpr char path::preferred_separator;

path& path::remove_filename() {
    if (type_ == Type::MULTI) {
        if (!cmpts_.empty()) {
            auto cmpt = std::prev(cmpts_.end());
            pathname_.erase(cmpt->pos_);
            cmpts_.erase(cmpt);
            trim();
        }
    }
    else
        clear();
    return *this;
}

path& path::replace_filename(const path& replacement) {
    remove_filename();
    operator/=(replacement);
    return *this;
}

path& path::replace_extension(const path& replacement) {
    auto ext = find_extension();
    if (ext.first && ext.second != string_type::npos)
    {
        if (ext.first == &pathname_)
            pathname_.erase(ext.second);
        else
        {
            const auto& back = cmpts_.back();
            if (ext.first != &back.pathname_)
                throw std::logic_error("path::replace_extension failed");
            pathname_.erase(back.pos_ + ext.second);
        }
    }
    if (!replacement.empty() && replacement.native()[0] != '.')
        pathname_ += '.';
    pathname_ += replacement.native();
    split_cmpts();
    return *this;
}

namespace {
template<typename Iter1, typename Iter2>
int do_compare(Iter1 begin1, Iter1 end1, Iter2 begin2, Iter2 end2) {
    int cmpt = 1;
    while (begin1 != end1 && begin2 != end2)
    {
        if (begin1->native() < begin2->native())
            return -cmpt;
        if (begin1->native() > begin2->native())
            return +cmpt;
        ++begin1;
        ++begin2;
        ++cmpt;
    }
    if (begin1 == end1)
    {
        if (begin2 == end2)
            return 0;
        return -cmpt;
    }
    return +cmpt;
}
}

int path::compare(const path& p) const noexcept {
    struct CmptRef
    {
        const path* ptr;
        const string_type& native() const noexcept { return ptr->native(); }
    };

    if (type_ == Type::MULTI && p.type_ == Type::MULTI)
        return do_compare(cmpts_.begin(), cmpts_.end(),
                          p.cmpts_.begin(), p.cmpts_.end());
    else if (type_ == Type::MULTI)
    {
        CmptRef c[1] = { { &p } };
        return do_compare(cmpts_.begin(), cmpts_.end(), c, c+1);
    }
    else if (p.type_ == Type::MULTI)
    {
        CmptRef c[1] = { { this } };
        return do_compare(c, c+1, p.cmpts_.begin(), p.cmpts_.end());
    }
    else
        return pathname_.compare(p.pathname_);
}

path path::root_name() const {
    path ret;
    if (type_ == Type::ROOT_NAME)
        ret = *this;
    else if (cmpts_.size() && cmpts_.begin()->type_ == Type::ROOT_NAME)
        ret = *cmpts_.begin();
    return ret;
}

path path::root_directory() const {
    path ret;
    if (type_ == Type::ROOT_DIR)
        ret = *this;
    else if (!cmpts_.empty())
    {
        auto iter = cmpts_.begin();
        if (iter->type_ == Type::ROOT_NAME)
            ++iter;
        if (iter != cmpts_.end() && iter->type_ == Type::ROOT_DIR)
            ret = *iter;
    }
    return ret;
}


path path::root_path() const
{
    path ret;
    if (type_ == Type::ROOT_NAME || type_ == Type::ROOT_DIR)
        ret = *this;
    else if (!cmpts_.empty())
    {
        auto iter = cmpts_.begin();
        if (iter->type_ == Type::ROOT_NAME) {
            ret = *iter++;
            if (iter != cmpts_.end() && iter->type_ == Type::ROOT_DIR) {
                ret.pathname_ += preferred_separator;
                ret.split_cmpts();
            }
        }
        else if (iter->type_ == Type::ROOT_DIR)
            ret = *iter;
    }
    return ret;
}

path path::relative_path() const
{
    path ret;
    if (type_ == Type::FILENAME)
        ret = *this;
    else if (!cmpts_.empty()) {
        auto iter = cmpts_.begin();
        if (iter->type_ == Type::ROOT_NAME)
            ++iter;
        if (iter != cmpts_.end() && iter->type_ == Type::ROOT_DIR)
            ++iter;
        if (iter != cmpts_.end())
            ret.assign(pathname_.substr(iter->pos_));
    }
    return ret;
}

path path::parent_path() const {
    path ret;
    if (cmpts_.size() < 2)
        return ret;
    for (auto iter = cmpts_.begin(), end_iter = std::prev(cmpts_.end());
         iter != end_iter; ++iter)
    {
        ret /= *iter;
    }
    return ret;
}

bool path::has_root_name() const {
    if (type_ == Type::ROOT_NAME)
        return true;
    if (!cmpts_.empty() && cmpts_.begin()->type_ == Type::ROOT_NAME)
        return true;
    return false;
}

bool path::has_root_directory() const {
    if (type_ == Type::ROOT_DIR)
        return true;
    if (!cmpts_.empty()) {
        auto iter = cmpts_.begin();
        if (iter->type_ == Type::ROOT_NAME)
            ++iter;
        if (iter != cmpts_.end() && iter->type_ == Type::ROOT_DIR)
            return true;
    }
    return false;
}

bool path::has_root_path() const {
    if (type_ == Type::ROOT_NAME || type_ == Type::ROOT_DIR)
        return true;
    if (!cmpts_.empty()) {
        auto type = cmpts_.front().type_;
        if (type == Type::ROOT_NAME || type == Type::ROOT_DIR)
            return true;
    }
    return false;
}

bool path::has_relative_path() const {
    if (type_ == Type::FILENAME)
        return true;
    if (!cmpts_.empty())
    {
        auto iter = cmpts_.begin();
        if (iter->type_ == Type::ROOT_NAME)
            ++iter;
        if (iter != cmpts_.end() && iter->type_ == Type::ROOT_DIR)
            ++iter;
        if (iter != cmpts_.end())
            return true;
    }
    return false;
}


bool path::has_parent_path() const {
    return cmpts_.size() > 1;
}

bool path::has_filename() const {
    return !empty();
}

std::pair<const std::string*, size_t> path::find_extension() const
{
    const string* s = nullptr;

    if (type_ != Type::MULTI)
        s = &pathname_;
    else if (!cmpts_.empty()) {
        const auto& c = cmpts_.back();
        if (c.type_ == Type::FILENAME)
            s = &c.pathname_;
    }

    if (s) {
        if (auto sz = s->size()) {
            if (sz <= 2 && (*s)[0] == '.') {
                if (sz == 1 || (*s)[1] == '.')  // filename is "." or ".."
                    return { s, string::npos };
                else
                    return { s, 0 };  // filename is like ".?"
            }
            return { s, s->rfind('.') };
        }
    }

    return {};
}

void
path::split_cmpts()
{
    type_ = Type::MULTI;
    cmpts_.clear();

    if (pathname_.empty())
        return;

    size_t pos = 0;
    const size_t len = pathname_.size();

    // look for root name or root directory
    if (is_dir_sep(pathname_[0]))
    {
        // look for root name, such as "//" or "//foo"
        if (len > 1 && pathname_[1] == pathname_[0])
        {
            if (len == 2) {
                // entire path is just "//"
                type_ = Type::ROOT_NAME;
                return;
            }

            if (!is_dir_sep(pathname_[2])) {
                // got root name, find its end
                pos = 3;
                while (pos < len && !is_dir_sep(pathname_[pos]))
                    ++pos;
                add_root_name(pos);
                if (pos < len) // also got root directory
                    add_root_dir(pos);
            }
            else
            {
                // got something like "///foo" which is just a root directory
                // composed of multiple redundant directory separators
                add_root_dir(0);
            }
        }
        else // got root directory
            add_root_dir(0);
        ++pos;
    }

    size_t back = pos;
    while (pos < len)
    {
        if (is_dir_sep(pathname_[pos]))
        {
            if (back != pos)
                add_filename(back, pos - back);
            back = ++pos;
        }
        else
            ++pos;
    }

    if (back != pos)
        add_filename(back, pos - back);
    else if (is_dir_sep(pathname_.back())) {
        // [path.itr]/8
        // "Dot, if one or more trailing non-root slash characters are present."
        if (cmpts_.back().type_ == Type::FILENAME) {
            const auto& last = cmpts_.back();
            pos = last.pos_ + last.pathname_.size();
            cmpts_.emplace_back(string_type(1, '.'), Type::FILENAME, pos);
        }
    }

    trim();
}

/* root name starts at the begging of the whole path. */
void path::add_root_name(size_t n) {
    cmpts_.emplace_back(pathname_.substr(0, n), Type::ROOT_NAME, 0);
}

/* root dir is a single char. */
void path::add_root_dir(size_t pos) {
    cmpts_.emplace_back(pathname_.substr(pos, 1), Type::ROOT_DIR, pos);
}

void path::add_filename(size_t pos, size_t n) {
    cmpts_.emplace_back(pathname_.substr(pos, n), Type::FILENAME, pos);
}

void path::trim() {
    /* Only one component, flatten it. */
    if (cmpts_.size() == 1) {
        type_ = cmpts_.front().type_;
        cmpts_.clear();
    }
}

std::size_t evm::fs::hash_value(const path& p) noexcept {
    // [path.non-member]
    // "If for two paths, p1 == p2 then hash_value(p1) == hash_value(p2)."
    // Equality works as if by traversing the range [begin(), end()), meaning
    // e.g. path("a//b") == path("a/b"), so we cannot simply hash pathname_
    // but need to iterate over individual elements. Use the hash_combine from
    // http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3876.pdf
    size_t seed = 0;
    for (const auto& x : p) {
        seed ^= std::hash<string>()(x.native()) + 0x9e3779b9
                + (seed<<6) + (seed>>2);
    }
    return seed;
}
